﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Kokomo.PeInspector.Reflection.Disassembly;
using Kokomo.Lexical.Coding;

namespace Kokomo.PeInspector.Reflection
{
	/// <summary>
	/// Converts code constructs to a lexical document.
	/// </summary>
	/// <remarks>
	/// This class builds a document using the provided <see cref="ICodeWriter"/>.
	/// There are a few convenience methods for writing normal text (<see cref="WriteText"/>)
	/// or keywords (<see cref="WriteKeywordText"/>, <see cref="WriteKeywordsWithTrailingSpace(string[])"/>)
	/// to this writer.
	/// <para>
	/// The <see cref="MemberContext"/>, <see cref="TypeContext"/>, <see cref="NamespaceContext"/>
	/// <see cref="ParentNamespaces"/> and <see cref="ImportedNamespaces"/> provide the context.
	/// Use <see cref="IsLocalType"/> to determine whether a type reference should be
	/// qualified.
	/// </para>
	/// <para>
	/// Implementations should raise <see cref="MemberStart"/>, <see cref="MemberEnd"/>,
	/// <see cref="MethodBodyStart"/>, <see cref="MethodBodyEnd"/>, <see cref="StatementStart"/>,
	/// <see cref="StatementHeaderEnd"/>, and <see cref="StatementEnd"/>
	/// events while building a document
	/// so that callers can record source information, such as the locations of
	/// statements within the source document.
	/// </para>
	/// </remarks>
	/// <see cref="ICodeWriter"/>
	public abstract class CodeDocumentBuilder
	{
		/// <summary>
		/// Gets the underlying <see cref="ICodeWriter"/>.
		/// </summary>
		public ICodeWriter Writer { get; private set; }
		/// <summary>
		/// Gets the delegate providing disassembly of a method.
		/// </summary>
		public Func<MethodBase, MethodDecompilation> DisassemblerDelegate { get; private set; }

		/// <summary>
		/// Gets the member context.
		/// </summary>
		/// <remarks>
		/// See remarks under <see cref="CodeDocumentBuilder"/> for usage notes.
		/// </remarks>
		public MemberInfo MemberContext { get; private set; }
		/// <summary>
		/// Gets the type context.
		/// </summary>
		/// <remarks>
		/// See remarks under <see cref="CodeDocumentBuilder"/> for usage notes.
		/// </remarks>
		public Type TypeContext { get; private set; }
		/// <summary>
		/// Gets the namespace context.
		/// </summary>
		/// <remarks>
		/// See remarks under <see cref="CodeDocumentBuilder"/> for usage notes.
		/// </remarks>
		public string NamespaceContext { get; private set; }
		/// <summary>
		/// Gets the list of namespaces surrounding the context.
		/// </summary>
		/// <remarks>
		/// See remarks under <see cref="CodeDocumentBuilder"/> for usage notes.
		/// </remarks>
		public string[] ParentNamespaces { get; private set; }
		/// <summary>
		/// Gets the list of namespaces imported into the context.
		/// </summary>
		/// <remarks>
		/// See remarks under <see cref="CodeDocumentBuilder"/> for usage notes.
		/// </remarks>
		public string[] ImportedNamespaces { get; private set; }

		/// <summary>
		/// Initializes a new instance of <see cref="CodeDocumentBuilder"/>.
		/// </summary>
		/// <param name="writer">The underlying <see cref="ICodeWriter"/>.</param>
		/// <param name="memberContext">The member context.</param>
		/// <param name="disassemblerDelegate">The delegate providing disassembly of a method</param>
		/// <param name="importedNamespaces">The list of namespaces imported into the context.</param>
		/// <exception cref="ArgumentNullException"><paramref name="writer"/> is <c>null</c>.</exception>
		public CodeDocumentBuilder(
			ICodeWriter writer,
			MemberInfo memberContext,
			Func<MethodBase, MethodDecompilation> disassemblerDelegate,
			string[] importedNamespaces = null
			)
		{
			if (writer == null) throw new ArgumentNullException("writer");

			this.Writer = writer;
			this.MemberContext = memberContext;
			this.DisassemblerDelegate = disassemblerDelegate;

			if (importedNamespaces == null) importedNamespaces = new string[0];
			this.ImportedNamespaces = importedNamespaces;

			this.DetermineContext();
		}

		private void DetermineContext()
		{
			if (this.MemberContext != null)
			{
				Type typeContext = this.MemberContext as Type;
				if (typeContext == null) typeContext = this.MemberContext.ReflectedType;
				this.TypeContext = typeContext;

				while (typeContext.ReflectedType != null) typeContext = typeContext.ReflectedType;

				string namespaceContext = typeContext.Namespace;
				if (namespaceContext != null)
				{
					this.NamespaceContext = namespaceContext;

					string[] namespaceParts = namespaceContext.Split('.');
					int parentNamespaceCount = namespaceParts.Length;
					string[] parentNamespaces = new string[parentNamespaceCount];
					for (int i = 0; i < parentNamespaceCount; i++)
					{
						string parentNamespace = string.Join(".", namespaceParts, 0, i + 1);
						parentNamespaces[i] = parentNamespace;
					}
					this.ParentNamespaces = parentNamespaces;
				}
			}
		}

		/// <summary>
		/// Determines whether a type is local to the context.
		/// </summary>
		/// <param name="type">The type to check.</param>
		/// <returns><c>true</c> if the type is local; otherwise, <c>false</c>.</returns>
		/// <remarks>
		/// This method checks whether the type is within the context identified by
		/// <see cref="MemberContext"/>, <see cref="TypeContext"/>,
		/// <see cref="ParentNamespaces"/>, or <see cref="ImportedNamespaces"/>.
		/// </remarks>
		public virtual bool IsLocalType(Type type)
		{
			if (type == null) throw new ArgumentNullException("type");

			bool isLocalType =
				(type.Namespace == this.NamespaceContext)
				|| ((this.TypeContext != null) && (type.ReflectedType == this.TypeContext))
				|| ((this.ParentNamespaces != null) && this.ParentNamespaces.Contains(type.Namespace))
				|| (this.ImportedNamespaces.Contains(type.Namespace))
				;

			return isLocalType;
		}

		/// <summary>
		/// Raised just before this instance writes the declaration of a member.
		/// </summary>
		public event EventHandler<MemberEventArgs> MemberStart;
		/// <summary>
		/// Raises the <see cref="MemberStart"/> event.
		/// </summary>
		/// <param name="member">The member to be written.</param>
		protected virtual void OnMemberStart(MemberInfo member)
		{
			if (this.MemberStart != null) this.MemberStart(this, new MemberEventArgs(member));
		}
		/// <summary>
		/// Raised just after this instance has written a member.
		/// </summary>
		public event EventHandler<MemberEventArgs> MemberEnd;
		/// <summary>
		/// Raises the <see cref="MemberEnd"/> event.
		/// </summary>
		/// <param name="member">The member written.</param>
		protected virtual void OnMemberEnd(MemberInfo member)
		{
			if (this.MemberEnd != null) this.MemberEnd(this, new MemberEventArgs(member));
		}
		/// <summary>
		/// Raised just before this instance writes a method body, just after the declaration.
		/// </summary>
		public event EventHandler<MethodBodyEventArgs> MethodBodyStart;
		/// <summary>
		/// Raises the <see cref="MethodBodyStart"/> event.
		/// </summary>
		/// <param name="method">The method to be written.</param>
		/// <param name="methodBody">The body of the method.</param>
		/// <param name="disassembly">The disassembly of the method.</param>
		protected virtual void OnMethodBodyStart(
			MethodBase method,
			MethodBody methodBody,
			MethodDecompilation disassembly
			)
		{
			if (this.MethodBodyStart != null) this.MethodBodyStart(this, new MethodBodyEventArgs(method, methodBody, disassembly));
		}


		public event EventHandler<MethodBodyEventArgs> MethodBodyEnd;
		protected virtual void OnMethodBodyEnd(
			MethodBase method,
			MethodBody methodBody,
			MethodDecompilation disassembly
			)
		{
			if (this.MethodBodyEnd != null) this.MethodBodyEnd(this, new MethodBodyEventArgs(method, methodBody, disassembly));
		}

		public event EventHandler<StatementEventArgs> StatementStart;
		protected virtual void OnStatementStart(Statement statement)
		{
			if (this.StatementStart != null) this.StatementStart(this, new StatementEventArgs(statement));
		}

		public event EventHandler<StatementEventArgs> StatementHeaderEnd;
		protected virtual void OnStatementHeaderEnd(Statement statement)
		{
			if (this.StatementHeaderEnd != null) this.StatementHeaderEnd(this, new StatementEventArgs(statement));
		}

		public event EventHandler<StatementEventArgs> StatementEnd;
		protected virtual void OnStatementEnd(Statement statement)
		{
			if (this.StatementEnd != null) this.StatementEnd(this, new StatementEventArgs(statement));
		}

		//public abstract void WriteMethodDeclaration(MethodInfo method);

		protected void WriteText(string text)
		{
			this.Writer.WriteText(text, CodeTextType.Normal);
		}
		protected void WriteKeywordText(string text)
		{
			this.Writer.WriteText(text, CodeTextType.Keyword);
		}
		protected void WriteKeywordWithTrailingSpace(string keyword)
		{
			if (keyword == null) throw new ArgumentNullException("keyword");

			this.WriteKeywordText(keyword);
			this.WriteText(" ");
		}
		protected void WriteKeywordsWithTrailingSpace(params string[] keywords)
		{
			this.WriteKeywordsWithTrailingSpace((IEnumerable<string>)keywords);
		}
		protected void WriteKeywordsWithTrailingSpace(IEnumerable<string> keywords)
		{
			if (keywords == null) throw new ArgumentNullException("keywords");

			//bool first = true;
			foreach (var keyword in keywords)
			{
				if (keyword == null) continue;

				//if (first == true) first = false;
				//else this.WriteText(" ");

				this.WriteKeywordWithTrailingSpace(keyword);
			}

			// Indicates whether anything was written
			//return !first;
		}
	}
}
